#!/usr/bin/env perl
use 5.010;
use strict;
use warnings;

=head1 NAME

20-testrunner-core.t - test testrunner's coredump handling

=head1 SYNOPSIS

  perl ./20-testrunner-core.t

This test will run the testrunner.pl script with some crashing processes
and do a basic verification that a backtrace of some kind was captured.

=cut

use Capture::Tiny qw( capture );
use English qw( -no_match_vars );
use File::Basename;
use File::Slurp qw( read_file );
use File::Temp qw( tempdir );
use FindBin;
use Readonly;
use Test::More;

use lib "$FindBin::Bin/../../lib/perl5";
use QtQA::Test::More qw( is_or_like );

# Matches backtrace text expected when a segfault occurs
Readonly my $SIGSEGV_BACKTRACE => qr{

    \A

    \QQtQA::App::TestRunner: Process exited due to signal 11; dumped core\E\n

    # We can't feasibly code the expected backtrace, so we just test that...
    #  (1) the core plugin claims to be producing a backtrace (backtrace follows:)
    QtQA::App::TestRunner: [^\n]+ backtrace[ ]follows:[^\n]+    \n

    .*

    #  (2) some message which is usually generated by gdb is present in the output
    \QQtQA::App::TestRunner: Thread 1\E

    .*

    \z

}xms;

# Testdata for core_pattern_to_glob_pattern
Readonly my @CORE_TO_GLOB_TESTDATA => (
    # trivial and default
    {
        core_pattern  => 'core',
        core_uses_pid => 0,
        glob_pattern  => 'core',
    },

    # pid on the end
    {
        core_pattern  => 'core',
        core_uses_pid => 1,
        glob_pattern  => 'core.*',
    },

    # pid on the end via %p
    {
        core_pattern  => 'core-%p',
        core_uses_pid => 0,
        glob_pattern  => 'core-*',
    },

    # literal %
    {
        core_pattern  => '/tmp/core%%foo/%e/bar',
        core_uses_pid => 0,
        glob_pattern  => '/tmp/core%foo/*',
    },

    # core_uses_pid makes no difference past the variable portion
    {
        core_pattern  => 'my-core-file:%t',
        core_uses_pid => 1,
        glob_pattern  => 'my-core-file:*',
    },

    # "piping core" behaves like `core'
    {
        core_pattern  => '|/usr/bin/some-cool-handler',
        core_uses_pid => 0,
        glob_pattern  => 'core',
    },

    # "piping core" with pid behaves like `core.%p'
    {
        core_pattern  => '|/usr/bin/some-cool-handler',
        core_uses_pid => 1,
        glob_pattern  => 'core.*',
    },
);

sub test_run
{
    my ($params_ref) = @_;

    my @args              = @{$params_ref->{ args }};
    my $expected_stdout   =   $params_ref->{ expected_stdout };
    my $expected_stderr   =   $params_ref->{ expected_stderr };
    my $expected_success  =   $params_ref->{ expected_success };
    my $expected_logfile  =   $params_ref->{ expected_logfile };
    my $expected_logtext  =   $params_ref->{ expected_logtext }  // "";
    my $testname          =   $params_ref->{ testname }          // q{};

    my $status;
    my ($output, $error) = capture {
        $status = system( 'perl', "$FindBin::Bin/../testrunner.pl", @args );
    };

    if ($expected_success) {
        is  ( $status, 0, "$testname exits zero" );
    }
    else {
        isnt( $status, 0, "$testname exits non-zero" );
    }

    is_or_like( $output, $expected_stdout, "$testname output looks correct" );
    is_or_like( $error,  $expected_stderr, "$testname error looks correct" );

    # The rest of the verification steps are only applicable if a log file is expected and created
    return if (!$expected_logfile);
    return if (!ok( -e $expected_logfile, "$testname created $expected_logfile" ));

    my $logtext = read_file( $expected_logfile );   # dies on error
    is_or_like( $logtext, $expected_logtext, "$testname logtext is as expected" );

    return;
}

sub test_testrunner
{
    # control; check that `--plugin core' can load OK
    test_run({
        testname         => 'plugin loads OK 0 exitcode',
        args             => [ '--plugin', 'core', '--', 'true' ],
        expected_success => 1,
        expected_stdout  => q{},
        expected_stderr  => q{},
    });

    # another control; check that it doesn't break non-zero exit code
    test_run({
        testname         => 'plugin loads OK !0 exitcode',
        args             => [ '--plugin', 'core', '--', 'false' ],
        expected_success => 0,
        expected_stdout  => q{},
        expected_stderr  => q{},
    });

    # check that a backtrace is generated if process crashes
    test_run({
        testname         => 'simple backtrace',
        args             => [ '--plugin', 'core', '--', 'perl', '-e', 'kill 11, $$' ],
        expected_success => 0,
        expected_stdout  => q{},
        expected_stderr  => $SIGSEGV_BACKTRACE,
    });


    # check that the backtrace is captured to log OK
    my $tempdir = tempdir( basename($0).'.XXXXXX', TMPDIR => 1, CLEANUP => 1 );
    test_run({
        testname         => 'backtrace to log',
        args             => [
            '--capture-logs',
            $tempdir,
            '--plugin',
            'core',
            '--sync-output',
            '--',
            'perl',
            '-e',
            'kill 11, $$'
        ],
        expected_success => 0,
        expected_stdout  => q{},
        expected_stderr  => q{},
        expected_logfile => "$tempdir/perl-00.txt",
        expected_logtext => $SIGSEGV_BACKTRACE,
    });

    # And again, with --tee
    test_run({
        testname         => 'backtrace to log',
        args             => [
            '--tee-logs',
            $tempdir,
            '--plugin',
            'core',
            '--',
            'perl',
            '-e',
            'kill 11, $$'
        ],
        expected_success => 0,
        expected_stdout  => q{},
        expected_stderr  => $SIGSEGV_BACKTRACE,
        expected_logfile => "$tempdir/perl-01.txt",
        expected_logtext => $SIGSEGV_BACKTRACE,
    });

    return;
}

sub test_core_pattern_to_glob_pattern
{
    ok( do("$FindBin::Bin/../testrunner-plugins/core.pm"), "plugin loaded OK" );

    my $plugin = QtQA::App::TestRunner::Plugin::core->new( );

    foreach my $row (@CORE_TO_GLOB_TESTDATA) {
        my $testname = "$row->{ core_pattern }, core_uses_pid $row->{ core_uses_pid }";

        my $expected_glob_pattern = $row->{ glob_pattern };
        my $actual_glob_pattern   = $plugin->_core_pattern_to_glob_pattern(
            $row->{ core_pattern },
            $row->{ core_uses_pid },
        );

        is( $actual_glob_pattern, $expected_glob_pattern, "glob pattern for $testname" );
    }

    return;
}

sub run
{
    if ($OSNAME !~ m{linux|darwin}i) {
        plan 'skip_all', "test is not relevant on $OSNAME";
    }

    if ($OSNAME =~ m{darwin}i && ! -w '/cores') {
        plan 'skip_all', 'you must make /cores directory writable to run this test on mac';
    }

    test_core_pattern_to_glob_pattern;
    test_testrunner;
    done_testing;

    return;
}

run if (!caller);
1;

